﻿using System;
using System.Linq;

namespace NeuralLib.Learning
{
    public class BackPropagationLearning : ISupervisedLearning
    {
        private double _error;
        private const double ErrorDivider = 2;
        private double _learningRate = 0.2;
        private readonly Network _network;

        private readonly double[][] _neuronErrors;

        private readonly double[][] _thresholdsUpdates;

        private readonly double[][][] _weightsUpdates;

        public BackPropagationLearning(Network network)
        {
            _network = network;

            _neuronErrors = new double[network.LayersCount][];
            _weightsUpdates = new double[network.LayersCount][][];
            _thresholdsUpdates = new double[network.LayersCount][];

            for (int i = 0, n = network.LayersCount; i < n; i++)
            {
                var layer = _network.Layers[i];

                _neuronErrors[i] = new double[layer.NeuronsCount];
                _weightsUpdates[i] = new double[layer.NeuronsCount][];
                _thresholdsUpdates[i] = new double[layer.NeuronsCount];

                for (var j = 0; j < layer.NeuronsCount; j++)
                {
                    _weightsUpdates[i][j] = new double[layer.InputsCount];
                }
            }
        }
        public double Error
        {
            get
            {
                return _error;
            }
        }

        public double LearningRate
        {
            get
            {
                return _learningRate;
            }
            set
            {
                if (value >= 0 && value <= 1)
                    _learningRate = value;
                else throw new ArgumentOutOfRangeException("_learningRate", "LearningRate shoud be between 0 and 1");
            }
        }
        public double Run(double[] input, double[] output)
        {
            if (input.Count() != _network.InputsCount)
                throw new ArgumentOutOfRangeException("input", "Input size don't equals network inputs count");
            if (output.Count() != _network.Output.Count())
                throw new ArgumentOutOfRangeException("output", "Output size don't equals network outputs count");

            _network.Compute(input);

            var error = GetNetworkError(output);

            GetWeightUpdates(input);

            UpdateNetwork();

            _error = error;

            return error;
        }

        private double GetNetworkError(double[] requiredOtput)
        {
            double error = 0;
            var layersCount = _network.LayersCount;

            var layer = _network.Layers[layersCount - 1];

            var function = layer.Neurons[0].ActivationFunction;
            var errors = _neuronErrors[layersCount - 1];

            for (int i = 0, n = layer.NeuronsCount; i < n; i++)
            {
                var output = layer.Neurons[i].Output;
                var e = requiredOtput[i] - output;
                errors[i] = e * function.Derivative2(output);
                error += (e * e);
            }

            for (var j = layersCount - 2; j >= 0; j--)
            {
                layer = _network.Layers[j];
                function = layer.Neurons[0].ActivationFunction;
                var layerNext = _network.Layers[j + 1];
                errors = _neuronErrors[j];
                var errorsNext = _neuronErrors[j + 1];

                for (int i = 0, n = layer.NeuronsCount; i < n; i++)
                {
                    var sum = 0.0;
                    for (int k = 0, m = layerNext.NeuronsCount; k < m; k++)
                    {
                        sum += errorsNext[k] * layerNext.Neurons[k].Weights[i];
                    }
                    errors[i] = sum * function.Derivative2(layer.Neurons[i].Output);
                }
            }

            return error / ErrorDivider;
        }

        private void GetWeightUpdates(double[] input)
        {
            Neuron neuron;

            double[] neuronWeightUpdates;

            var layer = _network.Layers[0];
            var layersErrors = _neuronErrors[0];
            var layerWeightsUpdates = _weightsUpdates[0];
            var layerThresholdUpdates = _thresholdsUpdates[0];

            double cachedError;

            for (int i = 0, n = layer.NeuronsCount; i < n; i++)
            {
                neuron = layer.Neurons[i];
                cachedError = layersErrors[i] * _learningRate;
                neuronWeightUpdates = layerWeightsUpdates[i];

                for (int j = 0, m = neuron.InputsCount; j < m; j++)
                    neuronWeightUpdates[j] = cachedError * input[j];

                layerThresholdUpdates[i] = cachedError;
            }

            for (int k = 1, l = _network.LayersCount; k < l; k++)
            {
                var layerPrev = _network.Layers[k - 1];
                layer = _network.Layers[k];
                layersErrors = _neuronErrors[k];
                layerWeightsUpdates = _weightsUpdates[k];
                layerThresholdUpdates = _thresholdsUpdates[k];

                for (int i = 0, n = layer.NeuronsCount; i < n; i++)
                {
                    neuron = layer.Neurons[i];
                    cachedError = layersErrors[i] * _learningRate;
                    neuronWeightUpdates = layerWeightsUpdates[i];

                    for (int j = 0, m = neuron.InputsCount; j < m; j++)
                        neuronWeightUpdates[j] = cachedError * layerPrev.Neurons[j].Output;

                    layerThresholdUpdates[i] = cachedError;
                }
            }
        }

        private void UpdateNetwork()
        {
            for (int i = 0, n = _network.LayersCount; i < n; i++)
            {
                var layer = _network.Layers[i];
                var layerWeightsUpdates = _weightsUpdates[i];
                var layerThresholdUpdates = _thresholdsUpdates[i];

                for (int j = 0, m = layer.NeuronsCount; j < m; j++)
                {
                    var neuron = layer.Neurons[j];
                    var neuronWeightUpdates = layerWeightsUpdates[j];

                    for (int k = 0, s = neuron.InputsCount; k < s; k++)
                        neuron.Weights[k] += neuronWeightUpdates[k];

                    neuron.Threshold += layerThresholdUpdates[j];
                }
            }
        }
       
    }
}